fix(meridian): rewrite hooks in Node, add cross-OS CI#3
Merged
Conversation
…bump to 0.10.4
The bash hooks crashed on macOS with "unexpected EOF while looking for
matching '". macOS ships bash 3.2, which misparses here-documents nested
in $(...) command substitution; the apostrophes in the orientation/audit
text ("user's", "doesn't") then read as an unterminated single quote.
Machines with a newer Homebrew bash first on PATH were unaffected, which
is why it reproduced on one laptop but not another.
Rewrite all three hooks (SessionStart, UserPromptSubmit, SessionEnd) in
Node and switch hooks.json to exec form (command: node, args: [path]).
Claude Code already requires Node, so the hooks now run identically on
macOS, Linux, and Windows with no shell involved -- eliminating the
bash-version trap, the BSD-vs-GNU sed/awk differences (BSD sed treated
s/\r//g as "delete every r", silently corrupting injected context), the
bashism set -o pipefail, and the hand-rolled JSON escaping. JSON.parse
and JSON.stringify now do all parsing and encoding.
The two large prompt texts move into hooks/context/*.md and are read at
runtime, so they need no shell/JSON escaping and are editable as plain
Markdown. session_id validation (it feeds rm -rf / mkdir paths) is
centralized in lib.mjs and still rejects anything non-UUID-shaped.
Add a GitHub Actions matrix (ubuntu, macos, windows) that runs the hooks the way Claude Code's exec form does -- node <hook> with the event JSON on stdin -- via a zero-dependency node:test harness, and runs `claude plugin validate --strict` on the marketplace and both plugin manifests. This is the regression guard for the bash 3.2 crash: the original failure was macOS-only, so the OS matrix is the point. The harness asserts the behaviors that were fragile before the Node rewrite: orientation/audit emit as valid JSON, the em-dash and the "user's" apostrophe survive the round trip, the every-8th-prompt audit cadence, 7-day prune, session cleanup, and rejection of unsafe session_id values before they reach the filesystem. --strict validation immediately caught real drift: marketplace.json still pinned meridian at 0.10.3 while plugin.json had moved to 0.10.4. Sync the marketplace entry to 0.10.4.
- Add an emitContextFile() helper to lib.mjs and route both hooks through it, removing the duplicated "resolve context/, read, trimEnd, emit" block and the node:url imports it required. - Hoist stateRoot() out of session-start's prune loop so it isn't recomputed per directory entry. - Lock the single-line-JSON invariant the rewrite exists to protect: a test now fails if the payload ever becomes multi-line (e.g. pretty-printed), which JSON.parse alone would not catch. - Reword the safeSessionId comment (it validates a single path segment, not a UUID) and give the remaining empty catches a one-line rationale to match the file's WHY-only convention. - Split CI: manifest validation runs once on ubuntu (it's OS-independent), while the OS matrix runs only the Node hook tests -- the real regression guard -- without a per-runner CLI install.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The plugin hooks crashed on macOS with
user-prompt-submit: line 39: unexpected EOF while looking for matching '. macOS ships bash 3.2, which misparses here-documents nested inside$(...)command substitution; the apostrophes in the orientation/audit text (user's,doesn't) then read as an unterminated single quote. Machines with a newer Homebrew bash first onPATHwere unaffected — which is why it reproduced on one laptop but not another.Rather than patch the bash, this rewrites all three hooks in Node and switches
hooks.jsonto exec form ("command": "node", "args": ["…"]), the cross-platform pattern the hooks docs recommend. Claude Code already requires Node, so the hooks now run identically on macOS, Linux, and Windows with no shell involved.Hooks (
plugins/meridian/hooks/)session-start.mjs,user-prompt-submit.mjs,session-end.mjs+ sharedlib.mjsreplace the bash scripts.JSON.parse/JSON.stringifydo all parsing and encoding.sed/awkdifferences (BSDsedtreateds/\r//gas "delete everyr", silently corrupting injected context on macOS), theset -o pipefailbashism, and the hand-rolled JSON escaper.hooks/context/*.mdand are read at runtime — no shell/JSON escaping, editable as plain Markdown.session_idvalidation (it feedsrm -rf/mkdirpaths) is centralized inlib.mjsand still rejects anything non-UUID-shaped.CI (
.github/workflows/ci.yml, new)node <hook>+ JSON on stdin) via a zero-dependencynode:testharness, and runsclaude plugin validate --stricton the marketplace + both plugin manifests.--strictvalidation immediately caught real drift:marketplace.jsonstill pinned meridian at0.10.3whileplugin.jsonhad moved to0.10.4. Synced to0.10.4.Version
meridianplugin0.10.3→0.10.4(so the marketplace surfaces the fix).Test plan
claude plugin validate --strict .passes locallyGenerated by Claude Code